/*
 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.eclipse.imagen.media.opimage;

import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.Point2D;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.IndexColorModel;
import java.awt.image.PixelInterleavedSampleModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.SinglePixelPackedSampleModel;
import java.awt.image.WritableRaster;
import java.util.Map;
import org.eclipse.imagen.GeometricOpImage;
import org.eclipse.imagen.ImageLayout;
import org.eclipse.imagen.JAI;
import org.eclipse.imagen.PackedImageData;
import org.eclipse.imagen.PixelAccessor;
import org.eclipse.imagen.media.util.ImageUtil;
import org.eclipse.imagen.media.util.JDKWorkarounds;

/**
 * A class extending <code>GeometricOpImage</code> to subsample binary images to gray scale images. Image scaling
 * operations require rectilinear backwards mapping and padding by the resampling filter dimensions.
 *
 * <p>When applying scale factors of scaleX, scaleY to a source image with width of src_width and height of src_height,
 * the resulting image is defined to have the following bounds: <code></pre>
 *       dst minX  = floor(src minX  * scaleX)
 *       dst minY  = floor(src minY  * scaleY)
 *       dst width  =  floor(src width  * scaleX)
 *       dst height =  floor(src height * scaleY)
 * </pre></code>
 *
 * @see ScaleOpImage
 */
public class SubsampleBinaryToGrayOpImage extends GeometricOpImage {

    /** The horizontal scale factor. */
    protected float scaleX;

    /** The vertical scale factor. */
    protected float scaleY;

    /** Cached value equal to 1/scaleX. */
    protected float invScaleX;

    /** Cached value equal to 1/scaleY. */
    protected float invScaleY;

    /** Used to determine whether a float is close to an int */
    private float floatTol;

    /** same as ceil(invScaleX), ceil(invScaleY) */
    private int blockX;

    private int blockY;

    /** destination image width */
    private int dWidth;
    /** destination image height */
    private int dHeight;

    /**
     * the 1st pixel location for destination pixels, i.e., the source pixel matrix Note the index runs from
     * 0..dstWidth-1 and 0..dstHeight-1 [yValues[j] yValues[j]+blockY-1] by [xValues[i] xValues[i]+blockX-1] will be
     * condensed to form pixel <code>i</code>th pixel in row <code>j</code>
     */
    private int[] xValues;

    private int[] yValues;

    // a look up table; lut[i] counts 1s in binary expression of i
    private int[] lut = new int[256];

    /**
     * Convert from number of bits on count to gray value, with scaling, i.e. if invScaleX,Y=3,3, then the possible bit
     * counts are 0..9, hence the lookup tables are [0..9] * 255/9.
     */
    protected byte[] lutGray;

    // package accessible for SubsampleBinaryToGrayOpImage4x4, etc...
    static ImageLayout layoutHelper(RenderedImage source, float scaleX, float scaleY, ImageLayout il, Map config) {

        ImageLayout layout = (il == null) ? new ImageLayout() : (ImageLayout) il.clone();

        // to compute dWidth and dHeight
        // fTol and dWi, dHi must be the same as in computeDestInfo(..)
        // due to static method, a few lines of coding are repeated
        int srcWidth = source.getWidth();
        int srcHeight = source.getHeight();

        float f_dw = scaleX * srcWidth;
        float f_dh = scaleY * srcHeight;
        float fTol = .1F * Math.min(scaleX / (f_dw + 1.0F), scaleY / (f_dh + 1.0F));

        int dWi = (int) (f_dw);
        int dHi = (int) (f_dh);

        // let it be int in the almost int case
        //   espacially in the true int case with float calculation errors
        if (Math.abs(Math.round(f_dw) - f_dw) < fTol) {
            dWi = Math.round(f_dw);
        }

        if (Math.abs(Math.round(f_dh) - f_dh) < fTol) {
            dHi = Math.round(f_dh);
        }

        // Set the top left coordinate of the destination
        layout.setMinX((int) (scaleX * source.getMinX()));
        layout.setMinY((int) (scaleY * source.getMinY()));

        layout.setWidth(dWi);
        layout.setHeight(dHi);

        // sample model
        SampleModel sm = layout.getSampleModel(null);

        if (sm == null
                || sm.getDataType() != DataBuffer.TYPE_BYTE
                || !(sm instanceof PixelInterleavedSampleModel
                        || sm instanceof SinglePixelPackedSampleModel && sm.getNumBands() == 1)) {

            // Width and height will be corrected in OpImage.layoutHelper
            sm = new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, 1, 1, 1, 1, new int[] {0});
        }

        layout.setSampleModel(sm);

        ColorModel cm = layout.getColorModel(null);

        if (cm == null || !JDKWorkarounds.areCompatibleDataModels(sm, cm)) {

            layout.setColorModel(ImageUtil.getCompatibleColorModel(sm, config));
        }

        return layout;
    }

    // Since this operation deals with packed bits in a binary image, we
    // do not need to expand the IndexColorModel
    private static Map configHelper(Map configuration) {

        Map config;

        if (configuration == null) {
            config = new RenderingHints(JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.FALSE);
        } else {

            config = configuration;

            if (!config.containsKey(JAI.KEY_REPLACE_INDEX_COLOR_MODEL)) {
                RenderingHints hints = (RenderingHints) configuration;
                config = (RenderingHints) hints.clone();
                config.put(JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.FALSE);
            }
        }

        return config;
    }

    /**
     * Constructs a <code>SubsampleBinaryToGrayOpImage</code> from a <code>RenderedImage</code> source, x and y scale
     * object. The image dimensions are determined by forward-mapping the source bounds, and are passed to the
     * superclass constructor by means of the <code>layout</code> parameter. Other fields of the layout are passed
     * through unchanged. If <code>layout</code> is <code>null</code>, a new <code>ImageLayout</code> will be
     * constructor to hold the bounds information.
     *
     * <p>The float rounding errors, such as 1.2 being internally represented as 1.200001, are dealt with the floatTol,
     * which is set up so that only 1/10 of pixel error will occur at the end of a line, which yields correct results
     * with Math.round() operation. The repeatability is guaranteed with a one-time computed table xvalues and yvalues.
     *
     * @param layout an <code>ImageLayout</code> optionally containing the tile grid layout, <code>SampleModel</code>,
     *     and <code>ColorModel</code>, or <code>null</code>.
     * @param source a <code>RenderedImage</code>. from this <code>OpImage</code>, or <code>null</code>. If <code>null
     *     </code>, no caching will be performed.
     * @param cobbleSources a boolean indicating whether <code>computeRect</code> expects contiguous sources.
     * @param extender a <code>BorderExtender</code>, or <code>null</code>.
     * @param interp an <code>Interpolation</code> object to use for resampling.
     * @param scaleX scale factor along x axis.
     * @param scaleY scale factor along y axis.
     * @throws IllegalArgumentException if combining the source bounds with the layout parameter results in negative
     *     output width or height.
     */
    public SubsampleBinaryToGrayOpImage(
            RenderedImage source, ImageLayout layout, Map config, float scaleX, float scaleY) {

        super(
                vectorize(source),
                layoutHelper(source, scaleX, scaleY, layout, config),
                configHelper(config),
                true, // cobbleSources,
                null, // extender
                null, // interpolation
                null);

        this.scaleX = scaleX;
        this.scaleY = scaleY;
        int srcMinX = source.getMinX();
        int srcMinY = source.getMinY();
        int srcWidth = source.getWidth();
        int srcHeight = source.getHeight();

        // compute floatTol, invScaleX, blockX, dWidth, dHeight,...
        computeDestInfo(srcWidth, srcHeight);

        if (extender == null) {
            computableBounds = new Rectangle(0, 0, dWidth, dHeight);
        } else {
            // If extender is present we can write the entire destination.
            computableBounds = getBounds();
        }

        // these can be delayed, such as placed in computeRect()
        buildLookupTables();

        // compute the begining bit position of each row and column
        computeXYValues(srcWidth, srcHeight, srcMinX, srcMinY);
    }

    /**
     * Computes the source point corresponding to the supplied point.
     *
     * @param destPt the position in destination image coordinates to map to source image coordinates.
     * @return a <code>Point2D</code> of the same class as <code>destPt</code>.
     * @throws IllegalArgumentException if <code>destPt</code> is <code>null</code>.
     * @since JAI 1.1.2
     */
    public Point2D mapDestPoint(Point2D destPt) {
        if (destPt == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        Point2D pt = (Point2D) destPt.clone();

        pt.setLocation(destPt.getX() / scaleX, destPt.getY() / scaleY);

        return pt;
    }

    /**
     * Computes the destination point corresponding to the supplied point.
     *
     * @param sourcePt the position in source image coordinates to map to destination image coordinates.
     * @return a <code>Point2D</code> of the same class as <code>sourcePt</code>.
     * @throws IllegalArgumentException if <code>sourcePt</code> is <code>null</code>.
     * @since JAI 1.1.2
     */
    public Point2D mapSourcePoint(Point2D sourcePt) {
        if (sourcePt == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        Point2D pt = (Point2D) sourcePt.clone();

        pt.setLocation(sourcePt.getX() * scaleX, sourcePt.getY() * scaleY);

        return pt;
    }

    /**
     * Returns the minimum bounding box of the region of the destination to which a particular <code>Rectangle</code> of
     * the specified source will be mapped.
     *
     * @param sourceRect the <code>Rectangle</code> in source coordinates.
     * @param sourceIndex the index of the source image.
     * @return a <code>Rectangle</code> indicating the destination bounding box, or <code>null</code> if the bounding
     *     box is unknown.
     * @throws IllegalArgumentException if <code>sourceIndex</code> is negative or greater than the index of the last
     *     source.
     * @throws IllegalArgumentException if <code>sourceRect</code> is <code>null</code>.
     */
    protected Rectangle forwardMapRect(Rectangle sourceRect, int sourceIndex) {

        if (sourceRect == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        if (sourceIndex != 0) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic1"));
        }

        // Get the source dimensions
        int x0 = sourceRect.x - blockX + 1;
        int y0 = sourceRect.y - blockY + 1;
        x0 = x0 < 0 ? 0 : x0;
        y0 = y0 < 0 ? 0 : y0;

        int dx0 = (int) (x0 * scaleX);
        int dy0 = (int) (y0 * scaleY);
        while (xValues[dx0] > x0 && dx0 > 0) {
            dx0--;
        }
        while (yValues[dy0] > y0 && dy0 > 0) {
            dy0--;
        }

        int x1 = sourceRect.x + sourceRect.width - 1;
        int y1 = sourceRect.y + sourceRect.height - 1;

        int dx1 = (int) Math.round(x1 * scaleX);
        int dy1 = (int) Math.round(y1 * scaleY);
        dx1 = dx1 >= dWidth ? dWidth - 1 : dx1;
        dy1 = dy1 >= dHeight ? dHeight - 1 : dy1;
        while (xValues[dx1] < x1 && dx1 < dWidth - 1) {
            dx1++;
        }
        while (yValues[dy1] < y1 && dy1 < dHeight - 1) {
            dy1++;
        }

        dx0 += this.minX;
        dy0 += this.minY;
        dx1 += this.minX;
        dy1 += this.minY;

        // Return the writable destination area
        return new Rectangle(dx0, dy0, dx1 - dx0 + 1, dy1 - dy0 + 1);
    }

    /**
     * Returns the minimum bounding box of the region of the specified source to which a particular <code>Rectangle
     * </code> of the destination will be mapped.
     *
     * @param destRect the <code>Rectangle</code> in destination coordinates.
     * @param sourceIndex the index of the source image.
     * @return a <code>Rectangle</code> indicating the source bounding box, or <code>null</code> if the bounding box is
     *     unknown.
     * @throws IllegalArgumentException if <code>sourceIndex</code> is negative or greater than the index of the last
     *     source.
     * @throws IllegalArgumentException if <code>destRect</code> is <code>null</code>.
     */
    protected Rectangle backwardMapRect(Rectangle destRect, int sourceIndex) {

        if (destRect == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        if (sourceIndex != 0) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic1"));
        }

        // Get the destination rectangle coordinates and dimensions
        // Note: indices starting from 0, thus minX/Y should be considered
        int sx0 = xValues[destRect.x - this.minX];
        int sy0 = yValues[destRect.y - this.minY];
        int sx1 = xValues[destRect.x - this.minX + destRect.width - 1];
        int sy1 = yValues[destRect.y - this.minY + destRect.height - 1];

        return new Rectangle(sx0, sy0, sx1 - sx0 + blockX, sy1 - sy0 + blockY);
    }

    /**
     * Performs a subsamplebinarytogray operation on a specified rectangle. The sources are cobbled.
     *
     * @param sources an array of source Rasters, guaranteed to provide all necessary source data for computing the
     *     output.
     * @param dest a WritableRaster containing the area to be computed.
     * @param destRect the rectangle within dest to be processed.
     */
    protected void computeRect(Raster[] sources, WritableRaster dest, Rectangle destRect) {
        Raster source = sources[0];

        switch (source.getSampleModel().getDataType()) {
            case DataBuffer.TYPE_BYTE:
            case DataBuffer.TYPE_SHORT:
            case DataBuffer.TYPE_USHORT:
            case DataBuffer.TYPE_INT:
                byteLoop(source, dest, destRect);
                break;
            default:
                throw new RuntimeException(JaiI18N.getString("SubsampleBinaryToGrayOpImage0"));
        }
    }

    private void byteLoop(Raster source, WritableRaster dest, Rectangle destRect) {
        PixelAccessor pa = new PixelAccessor(source.getSampleModel(), null);
        PackedImageData pid = pa.getPackedPixels(source, source.getBounds(), false, false);
        byte[] sourceData = pid.data;
        int sourceDBOffset = pid.offset;
        int dx = destRect.x;
        int dy = destRect.y;
        int dwi = destRect.width;
        int dhi = destRect.height;
        int sourceTransX = pid.rect.x; // source.getSampleModelTranslateX();
        int sourceTransY = pid.rect.y; // source.getSampleModelTranslateY();

        PixelInterleavedSampleModel destSM = (PixelInterleavedSampleModel) dest.getSampleModel();
        DataBufferByte destDB = (DataBufferByte) dest.getDataBuffer();
        int destTransX = dest.getSampleModelTranslateX();
        int destTransY = dest.getSampleModelTranslateY();
        int destScanlineStride = destSM.getScanlineStride();

        byte[] destData = destDB.getData();
        int destDBOffset = destDB.getOffset();

        int[] sbytenum = new int[dwi];
        int[] sstartbit = new int[dwi];

        int[] sAreaBitsOn = new int[dwi];
        for (int i = 0; i < dwi; i++) {
            int x = xValues[dx + i - this.minX];
            int sbitnum = pid.bitOffset + (x - sourceTransX);
            sbytenum[i] = sbitnum >> 3;
            sstartbit[i] = sbitnum % 8;
        }

        for (int j = 0; j < dhi; j++) {

            for (int i = 0; i < dwi; i++) {
                sAreaBitsOn[i] = 0;
            }

            for (int y = yValues[dy + j - this.minY]; y < yValues[dy + j - this.minY] + blockY; y++) {

                int sourceYOffset = (y - sourceTransY) * pid.lineStride + sourceDBOffset;

                int delement = 0, selement, sendbiti, sendbytenumi;
                for (int i = 0; i < dwi; i++) {
                    delement = 0;
                    sendbiti = sstartbit[i] + blockX - 1;
                    sendbytenumi = sbytenum[i] + (sendbiti >> 3);
                    sendbiti %= 8;

                    selement = 0x00ff & (int) sourceData[sourceYOffset + sbytenum[i]];

                    if (sbytenum[i] == sendbytenumi) {
                        selement <<= 24 + sstartbit[i];
                        selement >>>= 31 - sendbiti + sstartbit[i];
                        delement += lut[selement];
                    } else {
                        selement <<= 24 + sstartbit[i];
                        selement >>>= 24;
                        delement += lut[selement];
                        for (int b = sbytenum[i] + 1; b < sendbytenumi; b++) {
                            selement = 0x00ff & (int) sourceData[sourceYOffset + b];
                            delement += lut[selement];
                        }

                        selement = 0x00ff & (int) sourceData[sourceYOffset + sendbytenumi];
                        selement >>>= 7 - sendbiti;
                        delement += lut[selement];
                    }
                    sAreaBitsOn[i] += delement;
                }
            }
            int destYOffset = (j + dy - destTransY) * destScanlineStride + destDBOffset;

            destYOffset += dx - destTransX;

            // update dest values for row j in raster
            for (int i = 0; i < dwi; i++) {
                destData[destYOffset + i] = lutGray[sAreaBitsOn[i]];
            }
        }
    }

    private void computeDestInfo(int srcWidth, int srcHeight) {

        // Inverse scale factors
        invScaleX = 1.0F / scaleX;
        invScaleY = 1.0F / scaleY;
        blockX = (int) Math.ceil(invScaleX);
        blockY = (int) Math.ceil(invScaleY);

        // calculate dst width and height
        float f_dw = scaleX * srcWidth;
        float f_dh = scaleY * srcHeight;
        floatTol = .1F * Math.min(scaleX / (f_dw + 1.0F), scaleY / (f_dh + 1.0F));

        dWidth = (int) (f_dw);
        dHeight = (int) (f_dh);

        // let it be int in the almost int case
        //   espacially in the true int case with float calculation errors
        if (Math.abs(Math.round(f_dw) - f_dw) < floatTol) {
            dWidth = Math.round(f_dw);
        }

        if (Math.abs(Math.round(f_dh) - f_dh) < floatTol) {
            dHeight = Math.round(f_dh);
        }

        if (Math.abs(Math.round(invScaleX) - invScaleX) < floatTol) {
            invScaleX = Math.round(invScaleX);
            blockX = (int) invScaleX;
        }

        if (Math.abs(Math.round(invScaleY) - invScaleY) < floatTol) {
            invScaleY = Math.round(invScaleY);
            blockY = (int) invScaleY;
        }
    }

    // buildLookupTables()
    // initializes variabes bitSet and lut
    // to be called mainly in the constructor
    private final void buildLookupTables() {
        // lut
        lut[0] = 0;
        lut[1] = 1;
        lut[2] = 1;
        lut[3] = 2;
        lut[4] = 1;
        lut[5] = 2;
        lut[6] = 2;
        lut[7] = 3;
        lut[8] = 1;
        lut[9] = 2;
        lut[10] = 2;
        lut[11] = 3;
        lut[12] = 2;
        lut[13] = 3;
        lut[14] = 3;
        lut[15] = 4;
        for (int i = 16; i < 256; i++) {
            lut[i] = lut[i & (0x0f)] + lut[(i >> 4) & (0x0f)];
        }

        // lutGray
        if (lutGray != null) return;
        lutGray = new byte[blockX * blockY + 1];
        for (int i = 0; i < lutGray.length; i++) {
            int tmp = (int) Math.round(255.0F * i / (lutGray.length - 1.0F));
            lutGray[i] = tmp > 255 ? (byte) 0xff : (byte) tmp;
        }

        // switch black-white if needed
        if (isMinWhite(this.getSourceImage(0).getColorModel()))
            for (int i = 0; i < lutGray.length; i++) lutGray[i] = (byte) (255 - (0xff & lutGray[i]));
    }

    // this function can be called
    // only after dWidth and dHeight has been set in the constructor
    // XY values should be computed and stored for repeatable behavior
    // taking care of non-zero minX, minY
    private void computeXYValues(int srcWidth, int srcHeight, int srcMinX, int srcMinY) {
        if (xValues == null || yValues == null) {
            xValues = new int[dWidth];
            yValues = new int[dHeight];
        }

        float tmp;
        for (int i = 0; i < dWidth; i++) {
            tmp = invScaleX * i;
            xValues[i] = (int) Math.round(tmp);
        }
        if (xValues[dWidth - 1] + blockX > srcWidth) {
            xValues[dWidth - 1]--;
        }

        for (int i = 0; i < dHeight; i++) {
            tmp = invScaleY * i;
            yValues[i] = Math.round(tmp);
        }
        if (yValues[dHeight - 1] + blockY > srcHeight) {
            yValues[dHeight - 1]--;
        }

        // if case the source MinX/Y are not zeros
        if (srcMinX != 0) for (int i = 0; i < dWidth; i++) xValues[i] += srcMinX;
        if (srcMinY != 0) for (int i = 0; i < dHeight; i++) yValues[i] += srcMinY;
    }

    // check to see whether an indexed colormodel is inverted
    // returns false if cm not IndexColorModel
    // red[0] = 0     returns false
    // red[0] = 255   returns true
    static boolean isMinWhite(ColorModel cm) {
        if (cm == null || !(cm instanceof IndexColorModel)) return false;

        byte[] red = new byte[256];
        ((IndexColorModel) cm).getReds(red);
        return (red[0] == (byte) 255 ? true : false);
    }
}
